﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Security.Permissions;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls.WebParts;
using System.Xml;
using System.Xml.XPath;
using System.Reflection;
using Com.FastSearch.SharePoint.Interfaces;
using Com.FastSearch.SharePoint.WebControls.ESPSearchWebParts.UI.Editors;

namespace Com.FastSearch.SharePoint.WebControls.ESPSearchWebParts.UI
{
    /// <summary>
    /// A web-part that renders a single navigator.
    /// It is possible to use scope navigators
    /// </summary>
    public class ESPWeightedNavigator : ESPSearchWebPart
    {
        #region Private variables

        private const string NAVIGATOR_PROPERTIES_TITLE = "Weighted Navigator Properties";
        private const string NAVIGATOR_ELEMENT = "Navigator";
        private const string ID_ELEMENT = "NavId";
        private const string NAME_ELEMENT = "Name";
        private const string APPLIED_ITEMS_ELEMENT = "AppliedItems";
        private const string ITEMS_ELEMENT = "Items";
        private const string ADDITIONAL_ITEMS_ELEMENT = "AdditionalItems";
        private const string ITEM_ELEMENT = "Item";
        private const string VALUE_ELEMENT = "Value";
        private const string UNIT_ELEMENT = "Unit";
        private const string COUNT_ELEMENT = "Count";
        private const string TOP_COUNT_ELEMENT = "TopCount";
        private const string REMOVE_HREF_ELEMENT = "RemoveHref";
        private const string ADD_HREF_ELEMENT = "AddHref";
        private const string SHOW_MORE_ELEMENT = "ShowMore";
        private const string RELATIVE_COUNT_PERCENTAGE = "RelativePercentage";
        private const string SHOW_GRAPHICS_PERCENTAGE = "ShowGraphicsPercentage";
        private const string IS_PERCENTAGE_NAVIGATOR = "IsPercentageNavigator";

        private const string SCRIPT_RENDERED_KEY = "ESPNavigatorScriptRendered";

        private int _numberOfModifiers = 5;
        private bool _showMoreLink = true;
        private bool _renderPercentageAsGraph = true;
        private string _navigatorName;
        private string _navigatorScope;
        private CountStrategy _count;
        private volatile IESPNavigatorDefinition _navigatorDefinition;

        internal IESPSearchResult SearchResult
        {
            get;
            set;
        }

        internal IESPNavigator Navigator
        {
            get;
            set;
        }

        #endregion

        /// <summary>
        /// The available strategies for presenting the number of navigator items in a navigator.
        /// Depending on the completeness of the navigator (having aggregated over the full result-set
        /// or just the top N hits), counts may be more or less accurate. It is therefore configurable
        /// how counts should be presented, as it may not be desirable to show hard numbers when they
        /// are inaccurate, but rather just show a percentage (an estimation) or no count at all.
        /// </summary>
        public enum CountStrategy
        {
            /// <summary>
            /// Show an exact count if the navigator is aggregated over the full result-set, or fall back to
            /// showing a percentage if the navigator is shallow and doesn't use the entire result-set. See
            /// <see cref="AlwaysPercentage"/> for details on the percentage.
            /// </summary>
            CountFallbackToPercentage,

            /// <summary>
            /// Show an exact count if the navigator is aggregated over the full result-set, or fall back to
            /// not showing any count if the navigator is shallow and doesn't use the entire result-set.
            /// </summary>
            CountFallbackToNone,

            /// <summary>
            /// Always show the reported number of hits. For shallow navigators combined with large result-sets,
            /// this will often show a way too small count.
            /// </summary>
            AlwaysCount,

            /// <summary>
            /// Always show the reported number of hits relative to the number of docs used for aggregation.
            /// For shallow navigators combined with large result-sets this may be a better approximation than
            /// the <see cref="AlwaysCount"/>, but the percentage may be way off for the entire resultset (as
            /// it only reports the percentagewise occurrence of the items in the first e.g. 100 documents of
            /// the result-set).
            /// </summary>
            AlwaysPercentage,

            /// <summary>
            /// Don't show a count.
            /// </summary>
            None,
        }

        #region WebPart Properties

        /// <summary>
        /// The number of items to show for the navigator. Additional items will be available
        /// via a link if <see cref="ShowMoreLink"/> is true.
        /// </summary>
        public int NumberOfModifiers
        {
            get { return _numberOfModifiers; }
            set { _numberOfModifiers = value; }
        }

        /// <summary>
        /// If enabled, items exceeding the number defined in <see cref="NumberOfModifiers"/>
        /// will be available via "Show more items..." link.
        /// </summary>
        public bool ShowMoreLink
        {
            get { return _showMoreLink; }
            set { _showMoreLink = value; }
        }

        /// <summary>
        /// If enabled, items exceeding the number defined in <see cref="NumberOfModifiers"/>
        /// will be available via "Show more items..." link.
        /// </summary>
        public bool RenderPercentageAsBar
        {
            get { return _renderPercentageAsGraph; }
            set { _renderPercentageAsGraph = value; }
        }        

        /// <summary>
        /// The name of the navigator rendered.
        /// </summary>
        public string NavigatorName
        {
            get { return _navigatorName; }
            set { _navigatorName = value; }
        }

        /// <summary>
        /// The scope to aggregate over, for scope navigators.
        /// </summary>
        public string NavigatorScope
        {
            get { return _navigatorScope; }
            set { _navigatorScope = value; }
        }

        /// <summary>
        /// The strategy for representing navigator item counts.
        /// </summary>
        public CountStrategy Count
        {
            get { return _count; }
            set { _count = value; }
        }

        #endregion

        #region Custom Editor Registering

        /// <summary>
        /// Create the editor parts to use when configuring the navigator.
        /// </summary>
        /// <returns>An EditorPartCollection with a <see cref="ESPNavigatorEditorPart"/></returns>
        public override EditorPartCollection CreateEditorParts()
        {
            List<EditorPart> customEditorPartCollection = new List<EditorPart>();
            ESPWeightedNavigatorEditorPart configEditor = new ESPWeightedNavigatorEditorPart(SearchRequest.ESPSearchView);
            configEditor.ID = "ESPWeightedNavigatorEditorPart" + ID;
            configEditor.Title = NAVIGATOR_PROPERTIES_TITLE;
            customEditorPartCollection.Add(configEditor);
            return new EditorPartCollection(customEditorPartCollection);
        }

        #endregion

        #region WebControl members

        /// <summary>
        /// Render the control to the provided writer. Will render the scripts and styles required
        /// for the control if they have't been already.
        /// </summary>
        /// <param name="writer">The writer to render to.</param>
        protected override void Render(HtmlTextWriter writer)
        {
            EnsureChildControls();
            
            if (HttpContext.Current.Items[SCRIPT_RENDERED_KEY] == null)
            {                
                RenderScripts(writer);
                HttpContext.Current.Items[SCRIPT_RENDERED_KEY] = true;
            }
            base.Render(writer);
        }      

        private static void RenderScripts(TextWriter writer)
        {
            writer.WriteLine("<div style=\"display:none;border:1px solid black;\" id=\"navigator_weights\"></div>");
            writer.WriteLine("<script type=\"text/javascript\">");
            Assembly assembly = Assembly.GetExecutingAssembly();
            // Get the js out of the assembly - Mark Xsl as an Embedded resource for this to work.
            Stream stream = assembly.GetManifestResourceStream("Com.FastSearch.SharePoint.WebControls.ESPSearchWebParts.src.js.DragDrop.js");
            StreamReader r = new StreamReader(stream);
            string myJS = r.ReadToEnd();
            writer.WriteLine(myJS);
            
            writer.WriteLine("function toggleMoreNavigatorItems(id, linkId, more, less) {");
            writer.WriteLine("    var div = document.getElementById(id);");
            writer.WriteLine("    var link = document.getElementById(linkId);");
            writer.WriteLine("    if (div) {");
            writer.WriteLine("        if (div.style.display == \"none\") {");
            writer.WriteLine("            div.style.display = \"block\";");
            writer.WriteLine("            if (link) { link.innerHTML = less; }");
            writer.WriteLine("        } else {");
            writer.WriteLine("            div.style.display = \"none\";");
            writer.WriteLine("            if (link) { link.innerHTML = more; }");
            writer.WriteLine("        }");
            writer.WriteLine("    }");
            writer.WriteLine("}");

            writer.WriteLine("function NavigatorChecked(obj,navName,url) {");
            writer.WriteLine("  var imageName = obj.src.substring(obj.src.lastIndexOf('/')+1)");
            writer.WriteLine("  if (imageName.indexOf('checked') == -1)");
            writer.WriteLine("  {");
            writer.WriteLine("      imageName = (imageName.split(\".\"))[0] + 'checked.gif' ;");
            writer.WriteLine("  }");
            writer.WriteLine("  else");
            writer.WriteLine("  {");
            writer.WriteLine("      imageName = imageName.substring(0,imageName.indexOf('checked')) + '.gif' ;");
            writer.WriteLine("  }");
            writer.WriteLine("  obj.src = obj.src.substring(0,obj.src.lastIndexOf('/')+1) + imageName");
            writer.WriteLine("  var varChecked = (obj.src.indexOf('checked')!=-1)?'true':'false' ;");
            writer.WriteLine("  fetchNav(navName, url, '400', '101', varChecked) ;");
            writer.WriteLine("}");

            writer.WriteLine("</script>");
        }
        
        #endregion

        #region DataFormWebPart members

        /// <summary>
        /// Get a handle to the xml document representing the navigator, to be rendered using XSLT.
        /// </summary>
        /// <param name="viewPath">not used</param>
        /// <returns>An XPathNavigator for the xml document</returns>
        [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
        protected override XPathNavigator GetXPathNavigator(string viewPath)
        {
            XmlDocument doc = new XmlDocument();
            if (Navigator != null)
            {
                XmlNode root = doc.AppendChild(doc.CreateElement(NAVIGATOR_ELEMENT));               
                XmlNode id = root.AppendChild(doc.CreateElement(ID_ELEMENT));
                id.InnerText = ID;
                XmlNode field = root.AppendChild(doc.CreateElement(NAME_ELEMENT));
                field.InnerText = Title;
                XmlNode unit = root.AppendChild(doc.CreateElement(UNIT_ELEMENT));
                unit.InnerText = Navigator.Unit;
                int appliedElements = AddItems(doc, root, APPLIED_ITEMS_ELEMENT, Navigator.AppliedNavigatorItems, true);
                int elements = AddItems(doc, root, ITEMS_ELEMENT, Navigator.NavigatorItems, false);
                if (elements > 0)
                {
                    AddShowMore(doc, root, Navigator, appliedElements, elements);
                }
            }
            return doc.CreateNavigator();
        }

        private int AddItems(XmlDocument doc, XmlNode root, string elementsTag, IEnumerable<IESPNavigatorItem> items, bool applied)
        {
            XmlNode elements = doc.CreateElement(elementsTag);

            double MaxNormalizedPercentage = 0;

            if (IsPercentageNavigator) {
                AddPercentageAndGraphicsAttribute(doc, root, RenderPercentageAsBar, true);
                foreach (IESPNavigatorItem item in items) {
                    double val = GetPercentageCount(item);
                    if (val > MaxNormalizedPercentage) {
                        MaxNormalizedPercentage = val;
                    }
                }                
            }

            int i = -1;
            foreach (IESPNavigatorItem item in items)
            {
                if (++i == NumberOfModifiers && !applied)
                {
                    root.AppendChild(elements);
                    if (!ShowMoreLink)
                    {
                        // Don't add an additional items element, but exit the foreach loop, after which
                        // the elements node will be added to the document.
                        return 0;
                    }
                    root.AppendChild(elements);
                    elements = doc.CreateElement(ADDITIONAL_ITEMS_ELEMENT);
                    if (IsPercentageNavigator)
                    {
                        AddPercentageAndGraphicsAttribute(doc, root, RenderPercentageAsBar, true);
                    }                    
                }
                XmlNode itemNode = elements.AppendChild(doc.CreateElement(ITEM_ELEMENT));

                XmlNode value = itemNode.AppendChild(doc.CreateElement(VALUE_ELEMENT));
                value.InnerText = item.DisplayValue;
             
                if (applied)
                {
                    XmlNode removeHref = itemNode.AppendChild(doc.CreateElement(REMOVE_HREF_ELEMENT));
                    NavigationHelperParameter navParameter = new NavigationHelperParameter(Page.Request);
                    NavigationHelper.RemoveParameter(navParameter, IsScopeNavigator() ? "scopenav" : "nav", item.GetNavigationParam(NavigatorDefinition.Scope, SearchRequest.NavSplitCharacter), ESPSearchGroupID);
                    NavigationHelper.RemoveParameter(navParameter, "start1", ESPSearchGroupID);
                    removeHref.InnerText = navParameter.RawUrl;
                }
                else
                {
                    XmlNode addHref = itemNode.AppendChild(doc.CreateElement(ADD_HREF_ELEMENT));
                    NavigationHelperParameter navParameter = new NavigationHelperParameter(Page.Request);
                    NavigationHelper.AddParameter(navParameter, IsScopeNavigator() ? "scopenav" : "nav", item.GetNavigationParam(NavigatorDefinition.Scope, SearchRequest.NavSplitCharacter), ESPSearchGroupID);
                    NavigationHelper.RemoveParameter(navParameter, "start1", ESPSearchGroupID);
                    addHref.InnerText = navParameter.RawUrl;

                    XmlNode count = itemNode.AppendChild(doc.CreateElement(COUNT_ELEMENT));
                    count.InnerText = GetCount(item);

                    if (IsPercentageNavigator)
                    {
                        XmlNode percCount = itemNode.AppendChild(doc.CreateElement(RELATIVE_COUNT_PERCENTAGE));
                        percCount.InnerText = GetRelativePercentageCount(item, MaxNormalizedPercentage);
                    }
                }

                XmlNode fieldElem = itemNode.AppendChild(doc.CreateElement("FieldName"));
                fieldElem.InnerText = item.FieldName;

                bool navChecked = false ;
                if (!string.IsNullOrEmpty(Page.Request.QueryString["XRANK"]))
                {
                    if (Page.Request.QueryString["XRANK"].Contains(item.FieldName +"~/~" + item.DisplayValue))
                    {
                        navChecked = true ;
                    }
                }
                XmlNode fieldNavChk = itemNode.AppendChild(doc.CreateElement("NavChecked"));
                fieldNavChk.InnerText = navChecked.ToString();

            }
            if (elements.ChildNodes.Count > 0)
            {
                root.AppendChild(elements);
            }

            return elements.ChildNodes.Count;
        }

        private void AddPercentageAndGraphicsAttribute(XmlDocument doc, XmlNode elements, bool showGrapicsPercentage, bool isPercentageNavigator)
        {
            XmlAttribute showGraphicsPercentag = doc.CreateAttribute(SHOW_GRAPHICS_PERCENTAGE);
            showGraphicsPercentag.Value = showGrapicsPercentage.ToString().ToLower();
            elements.Attributes.Append(showGraphicsPercentag);

            XmlAttribute isPerceNavAttr = doc.CreateAttribute(IS_PERCENTAGE_NAVIGATOR);
            isPerceNavAttr.Value = isPercentageNavigator.ToString().ToLower();
            elements.Attributes.Append(isPerceNavAttr);                
                
        }
        
        private void AddShowMore(XmlDocument doc, XmlNode root, IESPNavigator navigator, int appliedElements, int elements)
        {
            if (navigator.ItemCount > NumberOfModifiers + appliedElements)
            {
                XmlNode showMore = root.AppendChild(doc.CreateElement(SHOW_MORE_ELEMENT));
                XmlNode count = showMore.AppendChild(doc.CreateElement(COUNT_ELEMENT));
                count.InnerText = elements.ToString();
                XmlNode topCount = showMore.AppendChild(doc.CreateElement(TOP_COUNT_ELEMENT));
                topCount.InnerText = (navigator.ItemCount - elements).ToString();
            }
        }

        private IESPNavigator GetNavigator()
        {
            if (NavigatorName == null)
            {
                Controls.Add(new LiteralControl("Navigator not configured."));
                return null;
            }
            return SearchResult == null ? null : SearchResult.GetNavigator(NavigatorName);
        }

        #endregion

        #region ESPSearchWebPart members

        /// <summary>
        /// Set any parameters on the search required by this web-part.
        /// </summary>
        protected override void ConfigureSearch()
        {
            if (NavigatorName != null)
            {
                IESPNavigatorDefinition navigator = NavigatorDefinition;
                if (navigator != null)
                {
                    if (navigator.IsScopeNavigator)
                    {
                        navigator.Scope = NavigatorScope ?? (Navigator != null ? Navigator.DisplayName : null);
                    }
                    SearchRequest.AddNavigator(navigator);
                }                
            }
        }

        /// <summary>
        /// Set the search-result. Called when the search has been performed.
        /// </summary>
        /// <param name="SearchResult">The search result</param>
        public override void SetSearchResult(IESPSearchResult SearchResult)
        {
            this.SearchResult = SearchResult;
            Navigator = GetNavigator();
        }

        /// <summary>
        /// Get the XSL stylesheet bundled with the web-part.
        /// </summary>
        /// <returns>The default XSL to be used when rendering this web-part</returns>
        public override string GetEmbeddedXsl()
        {
            return LoadXslFromAssembly("WeightedNavigator.xslt");
        }

        #endregion

        #region Private helpers

        internal string GetCount(IESPNavigatorItem item)
        {
            switch (Count)
            {
                case CountStrategy.CountFallbackToPercentage :
                    return IsCountExact() ? GetCountAsNumber(item) : GetCountAsPercentage(item);
                case CountStrategy.CountFallbackToNone :
                    return IsCountExact() ? GetCountAsNumber(item) : "";
                case CountStrategy.AlwaysCount :
                    return GetCountAsNumber(item);
                case CountStrategy.AlwaysPercentage :
                    return GetCountAsPercentage(item);
                case CountStrategy.None :
                default :
                    return "";
            }
        }

        internal bool IsPercentageNavigator
        {
            get {
                switch (Count)
                {
                    case CountStrategy.CountFallbackToPercentage:
                        return IsCountExact() ? false : true;
                    case CountStrategy.AlwaysPercentage:
                        return true;
                    case CountStrategy.None:
                    default:
                        return false;
                }
            }
        }

        internal double GetPercentageCount(IESPNavigatorItem item)
        {
            return 100.0 * item.ResultCount / Navigator.HitsUsed;
        }


        internal string GetRelativePercentageCount(IESPNavigatorItem item, double normalizedMaxPercentage)
        {
            switch (Count)
            {
                case CountStrategy.CountFallbackToPercentage:
                    return IsCountExact() ? "" : GetCountAsRelativePercentage(item, normalizedMaxPercentage);                
                case CountStrategy.AlwaysPercentage:
                    return GetCountAsRelativePercentage(item, normalizedMaxPercentage);
                case CountStrategy.None:
                default:
                    return "";
            }
        }

        internal bool IsCountExact()
        {
            return Navigator.HitsUsed == SearchResult.DocCount;
        }

        internal string GetCountAsNumber(IESPNavigatorItem item)
        {
            StringBuilder sb = new StringBuilder(10);
            if (!IsCountExact())
            {
                sb.Append('~');
            }
            sb.Append(item.ResultCount);
            return sb.ToString();
        }

        internal string GetCountAsRelativePercentage(IESPNavigatorItem item, double maxMaxPercentage) {
            double percentage = GetPercentageCount(item);
            if (percentage != 0)
            {
                return Convert.ToInt32(percentage/maxMaxPercentage * 100).ToString();
            }
            return "0";
        }

        internal string GetCountAsPercentage(IESPNavigatorItem item)
        {
            StringBuilder sb = new StringBuilder(10);
            if (!IsCountExact())
            {
                sb.Append('~');
            }
            sb.Append((100.0 * item.ResultCount / Navigator.HitsUsed).ToString("F2", CultureInfo.InvariantCulture));
            sb.Append('%');
            return sb.ToString();
        }

        internal IESPNavigatorDefinition NavigatorDefinition
        {
            get
            {
                if (_navigatorDefinition == null)
                {
                    lock (this)
                    {
                        if (_navigatorDefinition == null)
                        {
                            IDictionary<string, IESPNavigatorDefinition> navigators = GetSearchManager().GetNavigatorsForView(SearchRequest.ESPSearchView);
                            if (navigators.ContainsKey(NavigatorName)) {
                                _navigatorDefinition = navigators[NavigatorName];
                            }                            
                        }
                    }
                }
                return _navigatorDefinition;
            }
            set
            {
                _navigatorDefinition = value;
            }
        }

        internal bool IsScopeNavigator()
        {
            return NavigatorDefinition != null && NavigatorDefinition.IsScopeNavigator;
        }

        #endregion
    }
}
